package util;

import static util.IoUtils.mkdirs;
import static java.io.File.separator;
import static java.lang.Boolean.parseBoolean;
import static java.lang.System.getProperty;

import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.PrintStream;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.imageio.ImageIO;

/**
 * In debug mode this class will gather performance intensive information to help detect issues.</br> In release mode it is harmless in performance point of
 * view.</br> Default mode is release, to enable debug mode define system property <code>-DdebugMode=true</code>
 * 
 * @author Mykhaylo Adamovych
 */
public class DebugUtils {
    public static final String SP_DEBUG_MODE = "debugMode";
    public static final String SP_DEBUG_DUMP_DIR = "debug.dump.dir";
    public static final String DEFAULT_MSG = String.format("please define -D%s=true", SP_DEBUG_MODE);
    private static boolean isDebugMode = parseBoolean(getProperty(SP_DEBUG_MODE, "false"));
    private static String dumpDir = getProperty(SP_DEBUG_DUMP_DIR, getProperty("user.dir") + separator + "debugDump");

    /**
     * Creates an image containing pixels read from the screen. This image does not include the mouse cursor.
     */
    private static final void createScreenCapture(String id) {
        try {
            BufferedImage image = new Robot().createScreenCapture(new Rectangle(Toolkit.getDefaultToolkit().getScreenSize()));
            File screenCaptureFile = new File(dumpDir, id + ".png");
            ImageIO.write(image, "png", mkdirs(screenCaptureFile));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static final void createThreadDump(String id) {
        // Intentionally didn't use advanced features of java.lang.management not to be bind to JDK extensions.
        File threadDumpFile = new File(dumpDir, id + ".txt");
        PrintStream ps = null;
        try {
            ps = new PrintStream(mkdirs(threadDumpFile));
            Map<Thread, StackTraceElement[]> stackTraces = Thread.getAllStackTraces();
            List<Thread> threads = new LinkedList<Thread>(stackTraces.keySet());
            Collections.sort(threads, new Comparator<Thread>() {
                @Override
                public int compare(Thread o1, Thread o2) {
                    return o1.getName().compareToIgnoreCase(o2.getName());
                }
            });
            for (Thread thread : threads) {
                StringBuffer threadSummary = new StringBuffer();
                threadSummary.append("\"" + thread.getName() + "\"");
                threadSummary.append(", " + thread.getState());
                if (thread.isDaemon())
                    threadSummary.append(", deamon");
                threadSummary.append(", prio=" + thread.getPriority());
                threadSummary.append(", tid=" + thread.getId());
                ps.println(threadSummary.toString());
                StackTraceElement[] stackTrace = stackTraces.get(thread);
                for (StackTraceElement element : stackTrace)
                    ps.println("\tat " + element);
                ps.println();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            DisposableUtils.closeQuietly(ps);
        }
    }

    /**
     * Creates thread dump and screen capture to directory specified by {@link #SP_DEBUG_DUMP_DIR} or to user.dir
     * 
     * @return id of the artifacts created.
     */
    public static final String dump() {
        if (!isDebugMode)
            return DEFAULT_MSG;
        String dumpId = String.valueOf(System.currentTimeMillis());
        createThreadDump(dumpId);
        createScreenCapture(dumpId);
        System.err.println("Created dump " + dumpId + " to " + dumpDir);
        return dumpId;
    }

    /**
     * Evaluates caller stack trace element.
     * 
     * @return caller stack trace element or default message, never null.
     */
    public static final String getCaller() {
        if (!isDebugMode)
            return DEFAULT_MSG;
        return new Exception().getStackTrace()[3].toString();
    }

    /**
     * Evaluates caller stack trace.
     * 
     * @return caller stack trace or default message, never null.
     */
    public static final String getCallerStack() {
        if (!isDebugMode)
            return DEFAULT_MSG;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        PrintStream os = new PrintStream(baos);
        new Exception().printStackTrace(os);
        return baos.toString().substring(baos.toString().indexOf(getCaller()));
    }

    /**
     * Says if debug mode is enabled.
     */
    public static final boolean isDebugMode() {
        return isDebugMode;
    }

    /**
     * Default mode is chosen from runtime configuration system property. Sometimes it is useful to change mode programmatically while running tests etc. Ensure
     * this method is not used in release.
     */
    public static final void setDebugMode(boolean enable) {
        isDebugMode = enable;
    }

    /**
     * Default mode is chosen from runtime configuration system property. Sometimes it is useful to change mode programmatically while running tests etc. Ensure
     * this method is not used in release.
     */
    public static final void setDumpDir(String path) {
        dumpDir = path;
    }

    /**
     * Throwable to String
     */
    public static final String toString(Throwable th) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        PrintStream os = new PrintStream(baos);
        th.printStackTrace(os);
        return baos.toString();
    }
}
